using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.IO;
using RideMe.Core;
using System.Threading;
using System.Text.RegularExpressions;

namespace RideMe.Core
{
	/// <summary>
	/// Watches a log file on a background thread, looking for runtime errors.
	/// Raises an ErrorFound event when it finds one.
	/// The default constructor will watch the development log.
	/// Call Start() to actively start (or resume) watching the log,
	/// and Stop() to pause (suspends the thread).
	/// IMPORTANT: Call Dispose() when you're no longer interested
	/// in watching the file.
	/// </summary>
	public class LogFileWatcher : IDisposable
	{
		private string _filename;
		private ManualResetEvent _startSignal;
		private ManualResetEvent _stopSignal;
		private ManualResetEvent _exitSignal;
		private object _padLock = new object();
		private bool _running = false;
		Thread _thread;

		/// <summary>
		/// Nested class to define custom event args.
		/// </summary>
		public class RailsLogFileEventArgs : EventArgs
		{
			public RailsLogFileEventArgs(string errorType, string message, string file, int line)
			{
				this._errorType = errorType;
				this._message = message;
				this._fileName = Path.Combine(ProjectService.ProjectPath, file.TrimStart('/').Replace('/', '\\'));
				this._lineNumber = line;
			}

            private string _errorType;

            public string ErrorType
            {
                get { return _errorType; }
            }

            private string _message;

            public string Message
            {
                get { return _message; }
            }

            private string _fileName;

            public string FileName
            {
                get { return _fileName; }
            }

            private int _lineNumber;

            public int LineNumber
            {
                get { return _lineNumber; }
            }

		}

		/// <summary>
		/// Raised when a Ruby or Rails error is detected in the log file.
		/// </summary>
		public event EventHandler<RailsLogFileEventArgs> ErrorFound;

		/// <summary>
		/// Default constructor.  Will watch the development log file.
		/// </summary>
		public LogFileWatcher()
		{
			string developmentLog = Path.Combine(ProjectService.ProjectPath, @"log\development.log");
			Init(developmentLog);
		}

		/// <summary>
		/// Constructor.  Pass the full path of the file you want to watch.
		/// </summary>
		/// <param name="filename"></param>
		public LogFileWatcher(string filename)
		{
			Init(filename);
		}

		/// <summary>
		/// Returns true if the watcher is active.
		/// </summary>
		public bool IsWatching
		{
			get
			{
				lock (_padLock)
				{
					return _running;
				}
			}

			protected set
			{
				lock (_padLock)
				{
					_running = value;
				}
			}
		}

		/// <summary>
		/// Starts watching the log file.
		/// </summary>
		public void Start()
		{
			if (IsWatching)
				return;

			lock (_padLock)
			{
				_startSignal.Set();
			}
		}

		/// <summary>
		/// Stops watching the log file.  Restart by calling Start again.
		/// </summary>
		public void Stop()
		{
			if (!IsWatching)
				return;

			lock (_padLock)
			{
				_stopSignal.Set();
			}
		}

		/// <summary>
		/// Sets up threads and thread-signaling mechanism.
		/// </summary>
		/// <param name="filename"></param>
		private void Init(string filename)
		{
			_filename = filename;

			CreateIfNecessary();

			_stopSignal = new ManualResetEvent(false);
			_startSignal = new ManualResetEvent(false);
			_exitSignal = new ManualResetEvent(false);

			_thread = new Thread(new ThreadStart(WatchFile));
			_thread.IsBackground = true;
			_thread.Name = "LogFileWatcher";
			_thread.Start();
		}

		private void CreateIfNecessary()
		{
			try
			{
				if (!File.Exists(_filename))
				{
					StreamWriter sw = File.CreateText(_filename);
					sw.Close();
				}
			}

			catch (Exception e)
			{
				Trace.WriteLine(e.Message);
			}
		}

		/// <summary>
		/// Watch the file, start/paused by thread signals.
		/// </summary>
		private void WatchFile()
		{
			WaitHandle[] stopWaiting = new WaitHandle[] { _startSignal, _exitSignal };
			WaitHandle[] stopReading = new WaitHandle[] { _stopSignal, _exitSignal };

			// wait for start, or exit
			while (0 == EventWaitHandle.WaitAny(stopWaiting))
			{
				IsWatching = true;

				try
				{
					FileStream f = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

					StreamReader reader = new StreamReader(f);

					//start at the end of the file
					long lastMaxOffset = reader.BaseStream.Length;

					while (EventWaitHandle.WaitTimeout == EventWaitHandle.WaitAny(stopReading, 500, true))
					{
						//if the file size has not changed, idle
						if (reader.BaseStream.Length == lastMaxOffset)
							continue;

						//seek to the last max offset
						reader.BaseStream.Seek(lastMaxOffset, SeekOrigin.Begin);

						//read out of the file until the EOF
						StringBuilder b = new StringBuilder();
						string line;
						while ((line = reader.ReadLine()) != null)
							b.AppendLine(line);

						Analyze(b.ToString());

						//update the last max offset
						lastMaxOffset = reader.BaseStream.Position;
					}


					reader.Close();
					f.Close();

					IsWatching = false;
					_startSignal.Reset();
					_stopSignal.Reset();
				}

				catch (FileNotFoundException)
				{
				}
			}

			_stopSignal.Reset();
			_startSignal.Reset();
			_exitSignal.Reset();
		}

		
		private Regex _templateError = new Regex(@"^\s*ActionView::TemplateError \((.+?)\) on line #(\d+) of (.+?):", RegexOptions.Compiled | RegexOptions.Multiline);
		private Regex _eRbError = new Regex(@"^\s*ActionView::TemplateError \(compile error.+\.\.(/app.+?):(\d+):", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);

		
		private void Analyze(string snippet)
		{
			try
			{
				Match m = _templateError.Match(snippet);

				if (m.Success)
				{
					if (ErrorFound != null)
					{
						ErrorFound(this, new RailsLogFileEventArgs("TemplateError", m.Groups[1].Value, m.Groups[3].Value, Convert.ToInt32(m.Groups[2].Value)));
					}
				}
				else
				{
					m = _eRbError.Match(snippet);
					if (m.Success)
					{
						ErrorFound(this, new RailsLogFileEventArgs("TemplateError", "compiler error", m.Groups[1].Value, Convert.ToInt32(m.Groups[2].Value)));
					}
				}
			}

			catch (Exception e)
			{
				Trace.WriteLine(e.Message);
			}
		}


		#region IDisposable Members

		/// <summary>
		/// Kill the background thread and cleanup everything.
		/// </summary>
		public void Dispose()
		{
			Stop();

			if (_thread != null)
			{
				_exitSignal.Set();
				_thread.Join(500);
				_thread = null;
			}
		}

		#endregion
	}
}
